fix: JSONB fields_for + validate submit wiring; feat: checkbox_group (#10 #11 #9) - #14
Merged
Conversation
…cope A Hash responds to #each_with_index, so a JSONB column rendered with nested_attributes: false was iterated as [key, value] pairs, emitting scope[assoc][0][field], [1], ... instead of a single scope[assoc][field]. Only genuine collections (Enumerable, not Hash) iterate now — Enumerable covers Array and ActiveRecord::Relation without a hard AR dependency; a Hash / single record / Struct / PORO falls through to the single-scope branch. ## Test Coverage - treats a Hash-backed association as a single nested scope, not a collection (asserts name="record[profile][phone]", no [0]/[1] indices) ## Verification - [x] bundle exec rubocop lib spec passes - [x] bundle exec rspec passes Refs #10
Form(validate: true) attached the forms--validations--form controller and novalidate, but no data-action wired its onSubmit handler — the controller connected yet onSubmit never fired, so submitting an invalid form was not blocked client-side. apply_validation_coordinator now emits `submit->forms--validations--form#onSubmit`, joined with any caller-supplied data-action. This is the idiomatic Stimulus wiring (an action binds the handler) and the only surface the Ruby suite can prove; the existing onSubmit class-field handler in form_controller.js is left unchanged (self-wiring in connect() would double-bind and fire onSubmit twice). ## Test Coverage - wires the submit handler via a data-action - preserves a caller-supplied data-action alongside the coordinator action ## Verification - [x] bundle exec rubocop lib spec passes - [x] bundle exec rspec passes Refs #11
The batched "tag/facet picker" shape: a set of checkboxes sharing one array-valued field name (scope[name][]) with a leading empty-array hidden field, checked state derived from the model's current value matched by each item's resolved value:. Rendered under both themes. f.checkbox_group(:tag_ids, Tag.all, value: :id, label: :name, variant: :pill, size: :sm) f.field :tag_ids, as: :checkbox_group, collection: Tag.all, value: :id, label: :name - Forms::CheckboxGroup delegates each box's markup to DaisyUI::Checkbox so the size modifier resolves to a literal, scanner-visible class; the pill variant styles the active chip via Tailwind has-[:checked]: (no JS). - Forms::Plain::CheckboxGroup inherits the binding contract and overrides only the rendering seams — bare inputs, zero styling, aria-invalid on the group. - Field#checkbox_group owns the model binding (value:/label: as Symbol or Proc); Form#checkbox_group + render_field_input dispatch + the :checkbox_group theme role in both maps give full parity with select/tag_field. ## Test Coverage - shared array name across every checkbox + empty-array hidden field - checked set derived from the model (value 1,3 checked; 2 not) - option id from field id + value; label: proc with slug fallback - label HTML-escaped (no injection); size: maps to checkbox-sm - field inference (as: :checkbox_group); plain-theme parity (zero classes) - :checkbox_group role mapped in both Theme.daisy and Theme.plain ## Verification - [x] bundle exec rubocop lib spec passes - [x] bundle exec rspec passes Refs #9
This was referenced Jul 11, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes three open issues in one branch (three commits) — bundled because all
three touch
lib/forms/form.rb, so separate branches would just conflict there.fix(shell): fields_for treats a Hash-backed association as a single scopefix(shell): wire the validation coordinator's submit handlerfeat(components): checkbox_group verb for array-valued associations#10 —
fields_foriterated a Hash (JSONB) associationA Hash responds to
#each_with_index, so a JSONB column rendered withnested_attributes: falsewas iterated as[key, value]pairs, emittingscope[assoc][0][field],[1], … instead of a singlescope[assoc][field].Now only genuine collections iterate —
Enumerable && !Hashcovers Array andActiveRecord::Relationwithout a hard ActiveRecord dependency.#11 —
Form(validate: true)never blocked submitThe coordinator controller and
novalidatewere attached, but nodata-actionwired its
onSubmithandler — the controller connected yetonSubmitneverfired.
apply_validation_coordinatornow emitssubmit->forms--validations--form#onSubmit, joined with any caller-supplieddata-action.#9 —
checkbox_groupverb (the tag/facet-picker shape)One array-valued field name (
user[tag_ids][]) + a leading empty-array hiddenfield, checked state derived from the model's current value, rendered under both
themes. The daisy leaf delegates each box to
DaisyUI::Checkbox(literal sizeclass); the Plain twin ships zero styling.
variant:is layout-only, no JS.Test Coverage
record[profile][phone], no[0]/[1].data-actionemitted; a callerdata-actionis preserved alongside it.option id derivation;
label:proc with fallback; HTML-escaping;size:class;
as: :checkbox_groupinference; plain-theme parity; role mapped in both themes.Full suite: 141 examples, 0 failures ·
rubocop lib specclean.Deviations & judgment calls
controller's
connect()). I chose option 1 (emit thedata-actionfromRuby) — it's the only surface the existing test suite can prove (no JS test
harness), and it's idiomatic Stimulus. Doing both would double-bind
onSubmit(fire twice), soform_controller.jsis left unchanged; itsexisting
onSubmitclass-field handler is reachable via the emitted action.Array || ActiveRecord::Relation. Iused
Enumerable && !Hash— coversAR::Relation(includes Enumerable)without a hard ActiveRecord dependency (the gem must boot without AR), and
still excludes the Hash-backed JSONB scope. A single record / Struct / PORO is
not Enumerable → single scope, unchanged.
record.tag_ids) isalready the raw values, so the checked set compares against them directly —
value:is not re-applied to them. This matches existingcollection_check_boxesbehavior (no type coercion; a string/integer id mismatch is a known, gem-wide
v1 limitation).
"checkbox-#{size}"would break theno-interpolated-class rule, so the daisy leaf delegates the checkbox markup to
DaisyUI::Checkboxwith a modifier symbol → literal registered class. ThePlain twin overrides
render_checkboxto a bare<input>.required:: a group sharing one array name can't satisfy the browser'srequiredon any single box, sorender_field_inputdrops it for:checkbox_group(validate server-side) — same posture as:tags.